<?php
/**
 * OpenID 2.0 Authentication for PHP
 *
 * PHP version 5
 *
 * LICENSE:
 *
 * Copyright (c) 2006-2007 Pádraic Brady <padraic.brady@yahoo.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *    * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *    * The name of the author may not be used to endorse or promote products
 *      derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @category    Authentication
 * @package     OpenID
 * @author      Pádraic Brady <padraic.brady@yahoo.com>
 * @license     http://opensource.org/licenses/bsd-license.php New BSD License
 * @version     $Id: DiscoveryHtml.php 57 2007-07-17 20:03:27Z padraic $
 * @link        http://
 */

/** OpenID */
require_once 'OpenID.php';

/** Validate */
require_once 'Validate.php';

/** HTTP_Request */
require_once 'HTTP/Request.php';

 /**
 * HTML Discovery takes place for OpenID 1.1, or when an OpenID 2.0 Yadis discovery
 * cycle fails. Should HTML Discovery fail to locate the required value key
 * data from the HTML document when using OpenID 2.0 we MUST fall back to OpenID 1.1
 * and override the current settings.
 *
 * @category   Zend
 * @package    OpenID
 * @copyright  Copyright (c) 2007 Pádraic Brady (http://blog.astrumfutura.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class OpenID_DiscoveryHtml
{

    /**
     * The response object containing the HTML body we will
     * parse for and OpenID service data
     *
     * @var Zend_Http_Response
     */
    protected $_response = null;

    /**
     * Set the response object upon which to perform HTML discovery
     *
     * @param Zend_Http_Response $response
     */
    public function setResponse(Zend_Http_Response $response)
    {
        $this->_response = $response;
    }

    /**
     * Perform HTML Discovery either on the existing Response body
     * or by requesting the optional URI parameter to get HTML content
     * on which to perform
     *
     * @param string $uri
     * @return array|boolean
     */
    public function discover($uri = null)
    {
        if (isset($this->_response)) {
            $body = $this->_response->getBody();
        } else {
            if (!isset($uri) || !Validate::uri($uri)) {
                require_once 'OpenID/Exception.php';
            throw new OpenID_Exception('Unable to complete HTML Service Discovery, no Claimed Identifier was supplied, or the Claimed Identifier is not a valid URI');
            }
            $body = $this->_get($uri);
        }
        $result = $this->parse($body);
        return $result;
    }

    /**
     * Note: This method may transparently downgrade the current OpenID version to
     * 1.1 in order to attempt a backwards compatible discovery of the Identity
     * Provider. This will in turn force a 1.1 style authentication without
     * programmer interference.
     *
     * Parse an HTML document. This will filter out the required HTML RELs and
     * return their relevant HREF values. It allows for invalid XHTML to a degree and
     * will cope with the example given at
     * {@link http://openid.net/specs/openid-authentication-2_0-11.html#anchor51}
     *
     * @param string $content
     * @return array|bool
     */
    public function parse($content)
    {
        $validity = true;
        $result = array();

        $html = new DOMDocument();
        $html->loadHTML($content);
        $head = $html->getElementsByTagName('head');
        if ($head->length == 0) {
            require_once 'OpenID/Exception.php';
            throw new OpenID_Exception('Unable to complete HTML Service Discovery, the user URI points to an HTML page without a HEAD element');
        }

        $links = $head->item(0)->getElementsByTagName('link');
        if ($links->length == 0) {
            require_once 'OpenID/Exception.php';
            throw new OpenID_Exception('Unable to complete HTML Service Discovery, the user URI points to an HTML page without LINK elements inside a HEAD element');
        }

        $provider = null;
        $local_id = null;
        if (OpenID::getVersion() == 2.0) {
            foreach ($links as $link) {
                $rel = strtolower($link->getAttribute('rel'));
                if (is_null($provider) && preg_match("/openid2.provider/i", $rel)) {
                    $provider = $link;
                } elseif (is_null($local_id) && preg_match("/openid2.local_id/i", $rel)) {
                    $local_id = $link;
                }
            }
            if (!is_null($provider)) {
                $return = array();
                $return['opEndpoint'] = $provider->getAttribute('href');
                if (!is_null($local_id)) {
                    $return['localId'] = $local_id->getAttribute('href');
                }
                return $return;
            } else {
                OpenID::setVersion(1.1);
            }
        }

        /**
         * At this point, if OpenID 2.0 was originally enabled, it is now
         * disabled and we have entered the backup OpenID 1.1 mode
         */

        // seems a bit smelly as the last block is almost the same...
        $server = null;
        $delegate = null;
        if (OpenID::getVersion() !== 2.0) {
            foreach ($links as $link) {
                $rel = strtolower($link->getAttribute('rel'));
                if (is_null($provider) && preg_match("/openid.server/i", $rel)) {
                    $server = $link;
                } elseif (is_null($local_id) && preg_match("/openid.delegate/i", $rel)) {
                    $delegate = $link;
                }
            }
            if (!is_null($server)) { // note: local_id is optional
                $return = array();
                $return['opEndpoint'] = $server->getAttribute('href');
                if (!is_null($delegate)) {
                    $return['localId'] = $delegate->getAttribute('href');
                }
            }
            return $return;
        }

        return false;
    }

    /**
     * Form a request to fetch the user's URI in a GET request. The resulting
     * response body is returned for parsing.
     *
     * @param string $uri
     * @return string
     */
    private function _get($uri)
    {
        $request = new HTTP_Request($uri);
        $request->setMethod(HTTP_REQUEST_METHOD_GET);
        $response = $request->sendRequest();
        if (PEAR::isError($response)) {
            require_once 'OpenID/Exception.php';
            throw new OpenID_Exception('Invalid response received: ' . $request->getResponseCode() . ' ' . $request->getMessage());
        }
        return $request->getResponseBody();
    }

}